package cn.karent.domain;

import cn.karent.common.Pageable;
import lombok.extern.slf4j.Slf4j;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.StringField;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.Term;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.*;
import org.apache.lucene.search.highlight.*;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.wltea.analyzer.lucene.IKAnalyzer;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author wanshengdao
 * @date 2025/4/15
 */
@Slf4j
@Service
public class LuceneIndexService implements InitializingBean, DisposableBean {

    public static final Pattern HIGHLIGHTER_PATTERN = Pattern.compile("<b>(.*?)</b>");
    public static final String LEFT_BOUND = "<b>";
    public static final String RIGHT_BOUND = "</b>";

    private Directory directory;

    private IndexWriter indexWriter;

    private DirectoryReader reader;

    private SearcherManager searcherManager;

    private final IKAnalyzer analyzer = new IKAnalyzer(false);

    @Value("${lucene-path}")
    private String lucenePath;

    @Override
    public void afterPropertiesSet() throws Exception {
        log.info("lucene resource init....");
        directory = FSDirectory.open(Paths.get(lucenePath));
        IndexWriterConfig config = new IndexWriterConfig(analyzer);
        indexWriter = new IndexWriter(directory, config);
        searcherManager = new SearcherManager(directory, null);
        reader = DirectoryReader.open(directory);
        log.info("lucene resource init finished!");
    }

    @Override
    public void destroy() throws Exception {
        log.info("start close lucene resource....");
        if (indexWriter != null) {
            indexWriter.close();
        }
        if (directory != null) {
            directory.close();
        }
        if (reader != null) {
            reader.close();
        }
        log.info("close lucene resource finished!");
    }

    public void addDocumentToIndex(Cook doc) {
        try {
            Document luceneDoc = new Document();
            luceneDoc.add(new StringField("id", doc.getId().toString(), Field.Store.YES));
            luceneDoc.add(new TextField("category", doc.getCategory(), Field.Store.YES));
            luceneDoc.add(new TextField("name", doc.getName(), Field.Store.YES));
            luceneDoc.add(new TextField("content", doc.getContent(), Field.Store.YES));
            indexWriter.addDocument(luceneDoc);
            indexWriter.commit();
        } catch (IOException e) {
            log.error("lucene写入失败: ", e);
        }
    }

    public Pageable<SearchContent> search(String searchField, Integer page, Integer num) throws Exception {
        QueryParser parser = new QueryParser("name", analyzer);
        Query query = parser.parse(QueryParser.escape(searchField));
        // 刷新索引
        searcherManager.maybeRefresh();
        IndexSearcher searcher = searcherManager.acquire();
        TopDocs results = searcher.search(query, page * num);
        List<SearchContent> resultList = new ArrayList<>();

        int start = (page - 1) * num;
        int end = (int) Math.min(start + num, results.totalHits.value);
        Pageable<SearchContent> pageable = new Pageable<>();
        pageable.setTotal((int) results.totalHits.value);
        pageable.setNum(num);
        pageable.setTotalPages((int) (results.totalHits.value / num));
        pageable.setPage(page);
        // 定义高亮组件
        Formatter formatter = new SimpleHTMLFormatter(LEFT_BOUND, RIGHT_BOUND);
        QueryScorer scorer = new QueryScorer(query, "name");
        Highlighter highlighter = new Highlighter(formatter, scorer);
        highlighter.setTextFragmenter(new SimpleFragmenter(20));
        for (int i = start; i < end; i++) {
            ScoreDoc scoreDoc = results.scoreDocs[i];
            Document hitDoc = searcher.doc(scoreDoc.doc);
            log.debug("name: {}, searchField: {}", hitDoc.get("name"), searchField);
            // 文本高亮
            List<String> bestFragments = getBestFragment(highlighter, hitDoc);
            SearchContent searchContent = SearchContent.builder()
                    .id(Integer.parseInt(hitDoc.get("id")))
                    .category(hitDoc.get("category"))
                    .name(hitDoc.get("name"))
                    .highlights(bestFragments)
                    .build();
            resultList.add(searchContent);
        }
        pageable.setData(resultList);
        return pageable;
    }

    private List<String> getBestFragment(Highlighter highlighter, Document hitDoc) throws Exception {
        List<String> bestFragments = new ArrayList<>();
        String bestFragment = highlighter.getBestFragment(analyzer, "name", hitDoc.get("name"));
        bestFragment = Optional.ofNullable(bestFragment).orElse(LEFT_BOUND + hitDoc.get("name") + RIGHT_BOUND);
        Matcher matcher = HIGHLIGHTER_PATTERN.matcher(bestFragment);
        while (matcher.find()) {
            bestFragments.add(matcher.group(1));
        }
        return bestFragments;
    }

    public void deleteDocument(Integer id) {
        try {
            Term term = new Term("id", id.toString());
            indexWriter.deleteDocuments(term);
            indexWriter.commit();
        } catch (IOException e) {
            throw new IllegalStateException("lucene删除失败", e);
        }
    }

    /**
     * 更新文档
     *
     * @param doc 文档
     */
    public void updateDocument(Cook doc) {
        deleteDocument(doc.getId());
        addDocumentToIndex(doc);
    }
}
